package com.jorado.ognl;

import ognl.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Member;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * @Auther: len.zhang
 * @Date: 2019/8/14 11:10
 * @Description:
 */
public class Expression {

    private static final Logger LOGGER = LoggerFactory.getLogger(Expression.class);

    private OgnlContext context;
    private Node node;
    private Map dataContext;

    public Expression() {
        this(new HashMap());
    }

    /**
     * 数据上下文
     *
     * @param data
     */
    public Expression(Map data) {
        this.dataContext = data;
        this.context = (OgnlContext) Ognl.createDefaultContext(this.dataContext, new DefaultMemberAccess(false));
        this.node = null;
        this.putData(data);
    }

    /**
     * 批量添加数据
     *
     * @param data
     * @return
     */
    public Expression putData(Map data) {
        this.dataContext.putAll(data);
        this.context.putAll(data);
        return this;
    }

    /**
     * 添加数据
     *
     * @param key
     * @param value
     * @return
     */
    public Expression putData(Object key, Object value) {
        this.dataContext.put(key, value);
        this.context.put(key, value);
        return this;
    }

    public <T, R> Expression putData(Object key, T value, Function<T, R> func) {
        R val = func.apply(value);
        return this.putData(key, val);
    }

    public <T> Expression putData(T value, Function<T, Map> func) {
        Map val = func.apply(value);
        return this.putData(val);
    }

    public <R> Expression putData(Object key, Supplier<R> func) {
        R val = func.get();
        return this.putData(key, val);
    }

    public Expression putData(Supplier<Map> func) {
        Map val = func.get();
        this.putData(val);
        return this;
    }

    public Expression removeData(Object... keys) {
        for (Object key : keys) {
            this.dataContext.remove(key);
            this.context.remove(key);
        }
        return this;
    }

    public Expression clearData() {
        this.dataContext.clear();
        this.context.clear();
        return this;
    }

    /**
     * 评估表达式，bool类型
     *
     * @param expression
     * @return
     */
    public boolean evaluateBool(String expression) {
        Boolean data = evaluate(expression, Boolean.class);
        if (data == null) {
            return false;
        }
        return data.booleanValue();
    }

    /**
     * 评估表达式，int类型
     *
     * @param expression
     * @return
     */
    public int evaluateInt(String expression) {
        Integer data = evaluate(expression, Integer.class);
        if (data == null) {
            return 0;
        }
        return data.intValue();
    }

    /**
     * 评估表达式，long类型
     *
     * @param expression
     * @return
     */
    public long evaluateLong(String expression) {
        Long data = evaluate(expression, Long.class);
        if (data == null) {
            return 0L;
        }
        return data.longValue();
    }

    /**
     * 评估表达式，double类型
     *
     * @param expression
     * @return
     */
    public double evaluateDouble(String expression) {
        Double data = evaluate(expression, Double.class);
        if (data == null) {
            return 0D;
        }
        return data.doubleValue();
    }

    /**
     * 评估表达式，String类型
     *
     * @param expression
     * @return
     */
    public String evaluateString(String expression) {
        String data = evaluate(expression, String.class);
        if (data == null) {
            return null;
        }
        return data;
    }

    /**
     * 评估表达式，Object类型
     *
     * @param expression
     * @return
     */
    public Object evaluateObject(String expression) {
        ExpressionResult result = evaluate(expression);
        if (!result.isOk()) {
            return null;
        }
        return result.getValue();
    }

    /**
     * 评估表达式，泛型
     *
     * @param expression
     * @param <T>
     * @return
     */
    public <T> T evaluate(String expression, Class<T> cls) {
        ExpressionResult data = evaluate(expression);
        if (!data.isOk()) {
            return null;
        }
        try {
            Object o = data.getValue();
            if (cls != null) {
                T result = Converter.cast(o, cls);
                if (result != null) {
                    return result;
                }
                return cls.cast(o);
            }
        } catch (Exception ex) {
            LOGGER.error(String.format("expression [%s] evaluateT error,%s", expression, ex.getMessage()), ex);
        }
        return null;
    }

    /**
     * 评估表达式
     *
     * @param expression
     * @return
     */
    public ExpressionResult evaluate(String expression) {
        long start = System.currentTimeMillis();
        ExpressionResult expressionResult = new ExpressionResult();
        try {
            if (Consts.isBlank(expression)) {
                throw new IllegalArgumentException("expression is null or empty");
            }
            Node node = NodeCache.getNode(expression);
            Object value = node.getValue(context, context.getRoot());
            expressionResult.setValue(value);
            expressionResult.setOk(true);
            this.node = node;
        } catch (Exception ex) {
            LOGGER.error(String.format("expression [%s] evaluate error,%s", expression, ex.getMessage()), ex);
            expressionResult.setError(ex.getMessage());
        } finally {
            long elapsed = System.currentTimeMillis() - start;
            expressionResult.setElapsed(elapsed);
        }
        return expressionResult;
    }

    public String getDump() {
        if (node == null) {
            return "node is null";
        }
        StringWriter writer = new StringWriter();
        ((SimpleNode) node).dump(new PrintWriter(writer), "   ");
        return writer.toString();
    }

    public Map getDataContext() {
        return dataContext;
    }

    public static class DefaultMemberAccess implements MemberAccess {
        public boolean allowPrivateAccess;
        public boolean allowProtectedAccess;
        public boolean allowPackageProtectedAccess;

        /*===================================================================
            Constructors
          ===================================================================*/
        public DefaultMemberAccess(boolean allowAllAccess) {
            this(allowAllAccess, allowAllAccess, allowAllAccess);
        }

        public DefaultMemberAccess(boolean allowPrivateAccess, boolean allowProtectedAccess, boolean allowPackageProtectedAccess) {
            super();
            this.allowPrivateAccess = allowPrivateAccess;
            this.allowProtectedAccess = allowProtectedAccess;
            this.allowPackageProtectedAccess = allowPackageProtectedAccess;
        }

        /*===================================================================
            Public methods
          ===================================================================*/
        public boolean getAllowPrivateAccess() {
            return allowPrivateAccess;
        }

        public void setAllowPrivateAccess(boolean value) {
            allowPrivateAccess = value;
        }

        public boolean getAllowProtectedAccess() {
            return allowProtectedAccess;
        }

        public void setAllowProtectedAccess(boolean value) {
            allowProtectedAccess = value;
        }

        public boolean getAllowPackageProtectedAccess() {
            return allowPackageProtectedAccess;
        }

        public void setAllowPackageProtectedAccess(boolean value) {
            allowPackageProtectedAccess = value;
        }

        /*===================================================================
            MemberAccess interface
          ===================================================================*/
        @Override
        public Object setup(Map context, Object target, Member member, String propertyName) {
            Object result = null;

            if (isAccessible(context, target, member, propertyName)) {
                AccessibleObject accessible = (AccessibleObject) member;

                if (!accessible.isAccessible()) {
                    result = Boolean.FALSE;
                    accessible.setAccessible(true);
                }
            }
            return result;
        }

        @Override
        public void restore(Map context, Object target, Member member, String propertyName, Object state) {
            if (state != null) {
                final AccessibleObject accessible = (AccessibleObject) member;
                final boolean stateboolean = ((Boolean) state).booleanValue();  // Using twice (avoid unboxing)
                if (!stateboolean) {
                    accessible.setAccessible(stateboolean);
                } else {
                    throw new IllegalArgumentException("Improper restore state [" + stateboolean + "] for target [" + target +
                            "], member [" + member + "], propertyName [" + propertyName + "]");
                }
            }
        }

        /**
         * Returns true if the given member is accessible or can be made accessible
         * by this object.
         */
        @Override
        public boolean isAccessible(Map context, Object target, Member member, String propertyName) {
            int modifiers = member.getModifiers();
            boolean result = Modifier.isPublic(modifiers);

            if (!result) {
                if (Modifier.isPrivate(modifiers)) {
                    result = getAllowPrivateAccess();
                } else {
                    if (Modifier.isProtected(modifiers)) {
                        result = getAllowProtectedAccess();
                    } else {
                        result = getAllowPackageProtectedAccess();
                    }
                }
            }
            return result;
        }
    }
}